package view.jme;

import static com.jme3.util.BufferUtils.destroyDirectBuffer;
import static com.jme3.util.BufferUtils.populateFromBuffer;
import static com.jme3.util.BufferUtils.setInBuffer;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.util.BufferUtils;

public class SilentTangentBinormalGenerator {

	private static final float ZERO_TOLERANCE = 0.0000001f;
	private static final Logger log = Logger.getLogger(SilentTangentBinormalGenerator.class.getName());
	private static float toleranceDot;
	public static boolean debug = false;

	static {
		setToleranceAngle(45);
	}

	private static class VertexInfo {
		public final Vector3f position;
		public final Vector3f normal;
		public final Vector2f texCoord;
		public final ArrayList<Integer> indices = new ArrayList<Integer>();

		public VertexInfo(Vector3f position, Vector3f normal, Vector2f texCoord) {
			this.position = position;
			this.normal = normal;
			this.texCoord = texCoord;
		}
	}

	/**
	 * Collects all the triangle data for one vertex.
	 */
	private static class VertexData {
		public final ArrayList<TriangleData> triangles = new ArrayList<TriangleData>();

		public VertexData() {
		}
	}

	/**
	 * Keeps track of tangent, binormal, and normal for one triangle.
	 */
	public static class TriangleData {
		public final Vector3f tangent;
		public final Vector3f binormal;
		public final Vector3f normal;
		public int[] index = new int[3];
		public int triangleOffset;

		public TriangleData(Vector3f tangent, Vector3f binormal, Vector3f normal) {
			this.tangent = tangent;
			this.binormal = binormal;
			this.normal = normal;
		}

		public void setIndex(int[] index) {
			for (int i = 0; i < index.length; i++) {
				this.index[i] = index[i];
			}
		}
	}

	private static List<VertexData> initVertexData(int size) {
		List<VertexData> vertices = new ArrayList<VertexData>(size);
		for (int i = 0; i < size; i++) {
			vertices.add(new VertexData());
		}
		return vertices;
	}

	public static void generate(Mesh mesh) {
		generate(mesh, true, false);
	}

	public static void generate(Spatial scene, boolean splitMirrored) {
		if (scene instanceof Node) {
			Node node = (Node) scene;
			for (Spatial child : node.getChildren()) {
				generate(child, splitMirrored);
			}
		} else {
			Geometry geom = (Geometry) scene;
			Mesh mesh = geom.getMesh();

			// Check to ensure mesh has texcoords and normals before generating
			if (mesh.getBuffer(Type.TexCoord) != null && mesh.getBuffer(Type.Normal) != null) {
				generate(geom.getMesh(), true, splitMirrored);
			}
		}
	}

	public static void generate(Spatial scene) {
		generate(scene, false);
	}

	public static void generate(Mesh mesh, boolean approxTangents, boolean splitMirrored) {
		int[] index = new int[3];
		Vector3f[] v = new Vector3f[3];
		Vector2f[] t = new Vector2f[3];
		for (int i = 0; i < 3; i++) {
			v[i] = new Vector3f();
			t[i] = new Vector2f();
		}

		if (mesh.getBuffer(Type.Normal) == null) {
			throw new IllegalArgumentException("The given mesh has no normal data!");
		}

		List<VertexData> vertices;
		switch (mesh.getMode()) {
			case Triangles:
				vertices = processTriangles(mesh, index, v, t, splitMirrored);
				if (splitMirrored) {
					splitVertices(mesh, vertices, splitMirrored);
				}
				break;
			case TriangleStrip:
				vertices = processTriangleStrip(mesh, index, v, t);
				break;
			case TriangleFan:
				vertices = processTriangleFan(mesh, index, v, t);
				break;
			default:
				throw new UnsupportedOperationException(mesh.getMode() + " is not supported.");
		}

		processTriangleData(mesh, vertices, approxTangents, splitMirrored);

		// if the mesh has a bind pose, we need to generate the bind pose for the tangent buffer
		if (mesh.getBuffer(Type.BindPosePosition) != null) {

			VertexBuffer tangents = mesh.getBuffer(Type.Tangent);
			if (tangents != null) {
				VertexBuffer bindTangents = new VertexBuffer(Type.BindPoseTangent);
				bindTangents.setupData(Usage.CpuOnly, 4, Format.Float, BufferUtils.clone(tangents.getData()));

				if (mesh.getBuffer(Type.BindPoseTangent) != null) {
					mesh.clearBuffer(Type.BindPoseTangent);
				}
				mesh.setBuffer(bindTangents);
				tangents.setUsage(Usage.Stream);
			}
		}
	}

	public static void generate(Mesh mesh, boolean approxTangents) {
		generate(mesh, approxTangents, false);
	}

	private static List<VertexData> processTriangles(Mesh mesh, int[] index, Vector3f[] v, Vector2f[] t, boolean splitMirrored) {
		IndexBuffer indexBuffer = mesh.getIndexBuffer();
		FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData();
		if (mesh.getBuffer(Type.TexCoord) == null) {
			throw new IllegalArgumentException("Can only generate tangents for " + "meshes with texture coordinates");
		}

		FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData();

		List<VertexData> vertices = initVertexData(vertexBuffer.limit() / 3);

		for (int i = 0; i < indexBuffer.size() / 3; i++) {
			for (int j = 0; j < 3; j++) {
				index[j] = indexBuffer.get(i * 3 + j);
				populateFromBuffer(v[j], vertexBuffer, index[j]);
				populateFromBuffer(t[j], textureBuffer, index[j]);
			}

			TriangleData triData = processTriangle(index, v, t);
			if (splitMirrored) {
				triData.setIndex(index);
				triData.triangleOffset = i * 3;
			}
			if (triData != null) {
				vertices.get(index[0]).triangles.add(triData);
				vertices.get(index[1]).triangles.add(triData);
				vertices.get(index[2]).triangles.add(triData);
			}
		}

		return vertices;
	}

	// Don't remove splitmirorred boolean,It's not used right now, but i intend to
	// make this method also split vertice with rotated tangent space and I'll
	// add another splitRotated boolean
	private static List<VertexData> splitVertices(Mesh mesh, List<VertexData> vertexData, boolean splitMirorred) {
		int nbVertices = mesh.getBuffer(Type.Position).getNumElements();
		List<VertexData> newVertices = new ArrayList<VertexData>();
		Map<Integer, Integer> indiceMap = new HashMap<Integer, Integer>();
		FloatBuffer normalBuffer = mesh.getFloatBuffer(Type.Normal);

		for (int i = 0; i < vertexData.size(); i++) {
			ArrayList<TriangleData> triangles = vertexData.get(i).triangles;
			Vector3f givenNormal = new Vector3f();
			populateFromBuffer(givenNormal, normalBuffer, i);

			ArrayList<TriangleData> trianglesUp = new ArrayList<TriangleData>();
			ArrayList<TriangleData> trianglesDown = new ArrayList<TriangleData>();
			for (int j = 0; j < triangles.size(); j++) {
				TriangleData triangleData = triangles.get(j);
				if (parity(givenNormal, triangleData.normal) > 0) {
					trianglesUp.add(triangleData);
				} else {
					trianglesDown.add(triangleData);
				}
			}

			// if the vertex has triangles with opposite parity it has to be split
			if (!trianglesUp.isEmpty() && !trianglesDown.isEmpty()) {
				log.log(Level.FINE, "Splitting vertex {0}", i);
				// assigning triangle with the same parity to the original vertex
				vertexData.get(i).triangles.clear();
				vertexData.get(i).triangles.addAll(trianglesUp);

				// creating a new vertex
				VertexData newVert = new VertexData();
				// assigning triangles with opposite parity to it
				newVert.triangles.addAll(trianglesDown);

				newVertices.add(newVert);
				// keep vertex index to fix the index buffers later
				indiceMap.put(nbVertices, i);
				for (TriangleData tri : newVert.triangles) {
					for (int j = 0; j < tri.index.length; j++) {
						if (tri.index[j] == i) {
							tri.index[j] = nbVertices;
						}
					}
				}
				nbVertices++;

			}

		}

		if (!newVertices.isEmpty()) {

			// we have new vertices, we need to update the mesh's buffers.
			for (Type type : VertexBuffer.Type.values()) {
				// skip tangent buffer as we're gonna overwrite it later
				if (type == Type.Tangent || type == Type.BindPoseTangent) {
					continue;
				}
				VertexBuffer vb = mesh.getBuffer(type);
				// Some buffer (hardware skinning ones) can be there but not
				// initialized, they must be skipped.
				// They'll be initialized when Hardware Skinning is engaged
				if (vb == null || vb.getNumComponents() == 0) {
					continue;
				}

				Buffer buffer = vb.getData();
				// IndexBuffer has special treatement, only swapping the vertex indices is needed
				if (type == Type.Index) {
					boolean isShortBuffer = vb.getFormat() == VertexBuffer.Format.UnsignedShort;
					for (VertexData vertex : newVertices) {
						for (TriangleData tri : vertex.triangles) {
							for (int i = 0; i < tri.index.length; i++) {
								if (isShortBuffer) {
									((ShortBuffer) buffer).put(tri.triangleOffset + i, (short) tri.index[i]);
								} else {
									((IntBuffer) buffer).put(tri.triangleOffset + i, tri.index[i]);
								}
							}
						}
					}
					vb.setUpdateNeeded();
				} else {
					// copy the buffer in a bigger one and append nex vertices to the end
					Buffer newVerts = VertexBuffer.createBuffer(vb.getFormat(), vb.getNumComponents(), nbVertices);
					if (buffer != null) {
						buffer.rewind();
						bulkPut(vb.getFormat(), newVerts, buffer);

						int index = vertexData.size();
						newVerts.position(vertexData.size() * vb.getNumComponents());
						for (int j = 0; j < newVertices.size(); j++) {
							int oldInd = indiceMap.get(index);
							for (int i = 0; i < vb.getNumComponents(); i++) {
								putValue(vb.getFormat(), newVerts, buffer, oldInd * vb.getNumComponents() + i);
							}
							index++;
						}
						vb.updateData(newVerts);
						// destroy previous buffer as it's no longer needed
						destroyDirectBuffer(buffer);
					}
				}
			}
			vertexData.addAll(newVertices);

			mesh.updateCounts();
		}

		return vertexData;
	}

	private static void bulkPut(VertexBuffer.Format format, Buffer buf1, Buffer buf2) {
		switch (format) {
			case Byte:
			case Half:
			case UnsignedByte:
				((ByteBuffer) buf1).put((ByteBuffer) buf2);
				break;
			case Short:
			case UnsignedShort:

				((ShortBuffer) buf1).put((ShortBuffer) buf2);
				break;

			case Int:
			case UnsignedInt:
				((IntBuffer) buf1).put((IntBuffer) buf2);
				break;
			case Float:

				((FloatBuffer) buf1).put((FloatBuffer) buf2);
				break;
			case Double:
				((DoubleBuffer) buf1).put((DoubleBuffer) buf2);
				break;

			default:
				throw new UnsupportedOperationException("Unrecoginized buffer format: " + format);
		}
	}

	private static void putValue(VertexBuffer.Format format, Buffer buf1, Buffer buf2, int index) {
		switch (format) {
			case Byte:
			case Half:
			case UnsignedByte:
				byte b = ((ByteBuffer) buf2).get(index);
				((ByteBuffer) buf1).put(b);
				break;
			case Short:
			case UnsignedShort:
				short s = ((ShortBuffer) buf2).get(index);
				((ShortBuffer) buf1).put(s);
				break;

			case Int:
			case UnsignedInt:
				int i = ((IntBuffer) buf2).get(index);
				((IntBuffer) buf1).put(i);
				break;
			case Float:
				float f = ((FloatBuffer) buf2).get(index);
				((FloatBuffer) buf1).put(f);
				break;
			case Double:
				double d = ((DoubleBuffer) buf2).get(index);
				((DoubleBuffer) buf1).put(d);
				break;
			default:
				throw new UnsupportedOperationException("Unrecoginized buffer format: " + format);
		}
	}

	private static List<VertexData> processTriangleStrip(Mesh mesh, int[] index, Vector3f[] v, Vector2f[] t) {
		IndexBuffer indexBuffer = mesh.getIndexBuffer();
		FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData();
		FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData();

		List<VertexData> vertices = initVertexData(vertexBuffer.limit() / 3);

		index[0] = indexBuffer.get(0);
		index[1] = indexBuffer.get(1);

		populateFromBuffer(v[0], vertexBuffer, index[0]);
		populateFromBuffer(v[1], vertexBuffer, index[1]);

		populateFromBuffer(t[0], textureBuffer, index[0]);
		populateFromBuffer(t[1], textureBuffer, index[1]);

		for (int i = 2; i < indexBuffer.size(); i++) {
			index[2] = indexBuffer.get(i);
			BufferUtils.populateFromBuffer(v[2], vertexBuffer, index[2]);
			BufferUtils.populateFromBuffer(t[2], textureBuffer, index[2]);

			boolean isDegenerate = isDegenerateTriangle(v[0], v[1], v[2]);
			TriangleData triData = processTriangle(index, v, t);

			if (triData != null && !isDegenerate) {
				vertices.get(index[0]).triangles.add(triData);
				vertices.get(index[1]).triangles.add(triData);
				vertices.get(index[2]).triangles.add(triData);
			}

			Vector3f vTemp = v[0];
			v[0] = v[1];
			v[1] = v[2];
			v[2] = vTemp;

			Vector2f tTemp = t[0];
			t[0] = t[1];
			t[1] = t[2];
			t[2] = tTemp;

			index[0] = index[1];
			index[1] = index[2];
		}

		return vertices;
	}

	private static List<VertexData> processTriangleFan(Mesh mesh, int[] index, Vector3f[] v, Vector2f[] t) {
		IndexBuffer indexBuffer = mesh.getIndexBuffer();
		FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData();
		FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData();

		List<VertexData> vertices = initVertexData(vertexBuffer.limit() / 3);

		index[0] = indexBuffer.get(0);
		index[1] = indexBuffer.get(1);

		populateFromBuffer(v[0], vertexBuffer, index[0]);
		populateFromBuffer(v[1], vertexBuffer, index[1]);

		populateFromBuffer(t[0], textureBuffer, index[0]);
		populateFromBuffer(t[1], textureBuffer, index[1]);

		for (int i = 2; i < vertexBuffer.limit() / 3; i++) {
			index[2] = indexBuffer.get(i);
			populateFromBuffer(v[2], vertexBuffer, index[2]);
			populateFromBuffer(t[2], textureBuffer, index[2]);

			TriangleData triData = processTriangle(index, v, t);
			if (triData != null) {
				vertices.get(index[0]).triangles.add(triData);
				vertices.get(index[1]).triangles.add(triData);
				vertices.get(index[2]).triangles.add(triData);
			}

			Vector3f vTemp = v[1];
			v[1] = v[2];
			v[2] = vTemp;

			Vector2f tTemp = t[1];
			t[1] = t[2];
			t[2] = tTemp;

			index[1] = index[2];
		}

		return vertices;
	}

	// check if the area is greater than zero
	private static boolean isDegenerateTriangle(Vector3f a, Vector3f b, Vector3f c) {
		return (a.subtract(b).cross(c.subtract(b))).lengthSquared() == 0;
	}

	public static TriangleData processTriangle(int[] index, Vector3f[] v, Vector2f[] t) {
		Vector3f edge1 = new Vector3f();
		Vector3f edge2 = new Vector3f();
		Vector2f edge1uv = new Vector2f();
		Vector2f edge2uv = new Vector2f();

		Vector3f tangent = new Vector3f();
		Vector3f binormal = new Vector3f();
		Vector3f normal = new Vector3f();

		t[1].subtract(t[0], edge1uv);
		t[2].subtract(t[0], edge2uv);
		float det = edge1uv.x * edge2uv.y - edge1uv.y * edge2uv.x;

		boolean normalize = false;
		if (Math.abs(det) < ZERO_TOLERANCE) {
//			log.log(Level.WARNING, "Colinear uv coordinates for triangle " + "[{0}, {1}, {2}]; tex0 = [{3}, {4}], " + "tex1 = [{5}, {6}], tex2 = [{7}, {8}]",
//					new Object[] { index[0], index[1], index[2], t[0].x, t[0].y, t[1].x, t[1].y, t[2].x, t[2].y });
			det = 1;
			normalize = true;
		}

		v[1].subtract(v[0], edge1);
		v[2].subtract(v[0], edge2);

		tangent.set(edge1);
		tangent.normalizeLocal();
		binormal.set(edge2);
		binormal.normalizeLocal();

		if (Math.abs(Math.abs(tangent.dot(binormal)) - 1) < ZERO_TOLERANCE) {
//			log.log(Level.WARNING, "Vertices are on the same line " + "for triangle [{0}, {1}, {2}].", new Object[] { index[0], index[1], index[2] });
		}

		float factor = 1 / det;
		tangent.x = (edge2uv.y * edge1.x - edge1uv.y * edge2.x) * factor;
		tangent.y = (edge2uv.y * edge1.y - edge1uv.y * edge2.y) * factor;
		tangent.z = (edge2uv.y * edge1.z - edge1uv.y * edge2.z) * factor;
		if (normalize) {
			tangent.normalizeLocal();
		}

		binormal.x = (edge1uv.x * edge2.x - edge2uv.x * edge1.x) * factor;
		binormal.y = (edge1uv.x * edge2.y - edge2uv.x * edge1.y) * factor;
		binormal.z = (edge1uv.x * edge2.z - edge2uv.x * edge1.z) * factor;
		if (normalize) {
			binormal.normalizeLocal();
		}

		tangent.cross(binormal, normal);
		normal.normalizeLocal();

		return new TriangleData(tangent, binormal, normal);
	}

	public static void setToleranceAngle(float angle) {
		if (angle < 0 || angle > 179) {
			throw new IllegalArgumentException("The angle must be between 0 and 179 degrees.");
		}
		toleranceDot = FastMath.cos(angle * FastMath.DEG_TO_RAD);
	}

	private static boolean approxEqual(Vector3f u, Vector3f v) {
		float tolerance = 1E-4f;
		return (FastMath.abs(u.x - v.x) < tolerance) && (FastMath.abs(u.y - v.y) < tolerance) && (FastMath.abs(u.z - v.z) < tolerance);
	}

	private static boolean approxEqual(Vector2f u, Vector2f v) {
		float tolerance = 1E-4f;
		return (FastMath.abs(u.x - v.x) < tolerance) && (FastMath.abs(u.y - v.y) < tolerance);
	}

	private static ArrayList<VertexInfo> linkVertices(Mesh mesh, boolean splitMirrored) {
		ArrayList<VertexInfo> vertexMap = new ArrayList<VertexInfo>();

		FloatBuffer vertexBuffer = mesh.getFloatBuffer(Type.Position);
		FloatBuffer normalBuffer = mesh.getFloatBuffer(Type.Normal);
		FloatBuffer texcoordBuffer = mesh.getFloatBuffer(Type.TexCoord);

		Vector3f position = new Vector3f();
		Vector3f normal = new Vector3f();
		Vector2f texCoord = new Vector2f();

		final int size = vertexBuffer.limit() / 3;
		for (int i = 0; i < size; i++) {

			populateFromBuffer(position, vertexBuffer, i);
			populateFromBuffer(normal, normalBuffer, i);
			populateFromBuffer(texCoord, texcoordBuffer, i);

			boolean found = false;
			// Nehon 07/07/2013
			// Removed this part, joining splitted vertice to compute tangent space makes no sense to me
			// separate vertice should have separate tangent space
			if (!splitMirrored) {
				for (int j = 0; j < vertexMap.size(); j++) {
					VertexInfo vertexInfo = vertexMap.get(j);
					if (approxEqual(vertexInfo.position, position) && approxEqual(vertexInfo.normal, normal) && approxEqual(vertexInfo.texCoord, texCoord)) {
						vertexInfo.indices.add(i);
						found = true;
						break;
					}
				}
			}
			if (!found) {
				VertexInfo vertexInfo = new VertexInfo(position.clone(), normal.clone(), texCoord.clone());
				vertexInfo.indices.add(i);
				vertexMap.add(vertexInfo);
			}
		}

		return vertexMap;
	}

	private static void processTriangleData(Mesh mesh, List<VertexData> vertices, boolean approxTangent, boolean splitMirrored) {
		ArrayList<VertexInfo> vertexMap = linkVertices(mesh, splitMirrored);

		FloatBuffer tangents = BufferUtils.createFloatBuffer(vertices.size() * 4);

		ColorRGBA[] cols = null;
		if (debug) {
			cols = new ColorRGBA[vertices.size()];
		}

		Vector3f tangent = new Vector3f();
		Vector3f binormal = new Vector3f();
		// Vector3f normal = new Vector3f();
		Vector3f givenNormal = new Vector3f();

		Vector3f tangentUnit = new Vector3f();
		Vector3f binormalUnit = new Vector3f();

		for (int k = 0; k < vertexMap.size(); k++) {
			float wCoord = -1;

			VertexInfo vertexInfo = vertexMap.get(k);

			givenNormal.set(vertexInfo.normal);
			givenNormal.normalizeLocal();

			TriangleData firstTriangle = vertices.get(vertexInfo.indices.get(0)).triangles.get(0);

			// check tangent and binormal consistency
			tangent.set(firstTriangle.tangent);
			tangent.normalizeLocal();
			binormal.set(firstTriangle.binormal);
			binormal.normalizeLocal();

			for (int i : vertexInfo.indices) {
				ArrayList<TriangleData> triangles = vertices.get(i).triangles;

				for (int j = 0; j < triangles.size(); j++) {
					TriangleData triangleData = triangles.get(j);

					tangentUnit.set(triangleData.tangent);
					tangentUnit.normalizeLocal();
					if (tangent.dot(tangentUnit) < toleranceDot) {
						// log.log(Level.WARNING,
						// "Angle between tangents exceeds tolerance "
						// + "for vertex {0}.", i);
						break;
					}

					if (!approxTangent) {
						binormalUnit.set(triangleData.binormal);
						binormalUnit.normalizeLocal();
						if (binormal.dot(binormalUnit) < toleranceDot) {
							// log.log(Level.WARNING,
							// "Angle between binormals exceeds tolerance "
							// + "for vertex {0}.", i);
							break;
						}
					}
				}
			}

			// find average tangent
			tangent.set(0, 0, 0);
			binormal.set(0, 0, 0);

			int triangleCount = 0;
			for (int i : vertexInfo.indices) {
				ArrayList<TriangleData> triangles = vertices.get(i).triangles;
				triangleCount += triangles.size();
				if (debug) {
					cols[i] = ColorRGBA.White;
				}

				for (int j = 0; j < triangles.size(); j++) {
					TriangleData triangleData = triangles.get(j);
					tangent.addLocal(triangleData.tangent);
					binormal.addLocal(triangleData.binormal);

				}
			}

			int blameVertex = vertexInfo.indices.get(0);

			if (tangent.length() < ZERO_TOLERANCE) {
				log.log(Level.WARNING, "Shared tangent is zero for vertex {0}.", blameVertex);
				// attempt to fix from binormal
				if (binormal.length() >= ZERO_TOLERANCE) {
					binormal.cross(givenNormal, tangent);
					tangent.normalizeLocal();
				} // if all fails use the tangent from the first triangle
				else {
					tangent.set(firstTriangle.tangent);
				}
			} else {
				tangent.divideLocal(triangleCount);
			}

			tangentUnit.set(tangent);
			tangentUnit.normalizeLocal();
			if (Math.abs(Math.abs(tangentUnit.dot(givenNormal)) - 1) < ZERO_TOLERANCE) {
				log.log(Level.WARNING, "Normal and tangent are parallel for vertex {0}.", blameVertex);
			}

			if (!approxTangent) {
				if (binormal.length() < ZERO_TOLERANCE) {
					log.log(Level.WARNING, "Shared binormal is zero for vertex {0}.", blameVertex);
					// attempt to fix from tangent
					if (tangent.length() >= ZERO_TOLERANCE) {
						givenNormal.cross(tangent, binormal);
						binormal.normalizeLocal();
					} // if all fails use the binormal from the first triangle
					else {
						binormal.set(firstTriangle.binormal);
					}
				} else {
					binormal.divideLocal(triangleCount);
				}

				binormalUnit.set(binormal);
				binormalUnit.normalizeLocal();
				if (Math.abs(Math.abs(binormalUnit.dot(givenNormal)) - 1) < ZERO_TOLERANCE) {
					log.log(Level.WARNING, "Normal and binormal are parallel for vertex {0}.", blameVertex);
				}

				if (Math.abs(Math.abs(binormalUnit.dot(tangentUnit)) - 1) < ZERO_TOLERANCE) {
					log.log(Level.WARNING, "Tangent and binormal are parallel for vertex {0}.", blameVertex);
				}
			}

			Vector3f finalTangent = new Vector3f();
			Vector3f tmp = new Vector3f();
			for (int i : vertexInfo.indices) {
				if (approxTangent) {
					// Gram-Schmidt orthogonalize
					finalTangent.set(tangent).subtractLocal(tmp.set(givenNormal).multLocal(givenNormal.dot(tangent)));
					finalTangent.normalizeLocal();

					wCoord = tmp.set(givenNormal).crossLocal(tangent).dot(binormal) < 0f ? -1f : 1f;

					tangents.put((i * 4), finalTangent.x);
					tangents.put((i * 4) + 1, finalTangent.y);
					tangents.put((i * 4) + 2, finalTangent.z);
					tangents.put((i * 4) + 3, wCoord);
				} else {
					tangents.put((i * 4), tangent.x);
					tangents.put((i * 4) + 1, tangent.y);
					tangents.put((i * 4) + 2, tangent.z);
					tangents.put((i * 4) + 3, wCoord);

					// setInBuffer(binormal, binormals, i);
				}
			}
		}
		tangents.limit(tangents.capacity());
		// If the model already had a tangent buffer, replace it with the regenerated one
		mesh.clearBuffer(Type.Tangent);
		mesh.setBuffer(Type.Tangent, 4, tangents);

		if (mesh.isAnimated()) {
			mesh.clearBuffer(Type.BindPoseNormal);
			mesh.clearBuffer(Type.BindPosePosition);
			mesh.clearBuffer(Type.BindPoseTangent);
			mesh.generateBindPose(true);
		}

		if (debug) {
			writeColorBuffer(vertices, cols, mesh);
		}
		mesh.updateBound();
		mesh.updateCounts();
	}

	private static void writeColorBuffer(List<VertexData> vertices, ColorRGBA[] cols, Mesh mesh) {
		FloatBuffer colors = BufferUtils.createFloatBuffer(vertices.size() * 4);
		colors.rewind();
		for (ColorRGBA color : cols) {
			colors.put(color.r);
			colors.put(color.g);
			colors.put(color.b);
			colors.put(color.a);
		}
		mesh.clearBuffer(Type.Color);
		mesh.setBuffer(Type.Color, 4, colors);
	}

	private static int parity(Vector3f n1, Vector3f n) {
		if (n1.dot(n) < 0) {
			return -1;
		} else {
			return 1;
		}

	}

	public static Mesh genTbnLines(Mesh mesh, float scale) {
		if (mesh.getBuffer(Type.Tangent) == null) {
			return genNormalLines(mesh, scale);
		} else {
			return genTangentLines(mesh, scale);
		}
	}

	public static Mesh genNormalLines(Mesh mesh, float scale) {
		FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData();
		FloatBuffer normalBuffer = (FloatBuffer) mesh.getBuffer(Type.Normal).getData();

		ColorRGBA originColor = ColorRGBA.White;
		ColorRGBA normalColor = ColorRGBA.Blue;

		Mesh lineMesh = new Mesh();
		lineMesh.setMode(Mesh.Mode.Lines);

		Vector3f origin = new Vector3f();
		Vector3f point = new Vector3f();

		FloatBuffer lineVertex = BufferUtils.createFloatBuffer(vertexBuffer.limit() * 2);
		FloatBuffer lineColor = BufferUtils.createFloatBuffer(vertexBuffer.limit() / 3 * 4 * 2);

		for (int i = 0; i < vertexBuffer.limit() / 3; i++) {
			populateFromBuffer(origin, vertexBuffer, i);
			populateFromBuffer(point, normalBuffer, i);

			int index = i * 2;

			setInBuffer(origin, lineVertex, index);
			setInBuffer(originColor, lineColor, index);

			point.multLocal(scale);
			point.addLocal(origin);
			setInBuffer(point, lineVertex, index + 1);
			setInBuffer(normalColor, lineColor, index + 1);
		}

		lineMesh.setBuffer(Type.Position, 3, lineVertex);
		lineMesh.setBuffer(Type.Color, 4, lineColor);

		lineMesh.setStatic();
		// lineMesh.setInterleaved();
		return lineMesh;
	}

	private static Mesh genTangentLines(Mesh mesh, float scale) {
		FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData();
		FloatBuffer normalBuffer = (FloatBuffer) mesh.getBuffer(Type.Normal).getData();
		FloatBuffer tangentBuffer = (FloatBuffer) mesh.getBuffer(Type.Tangent).getData();

		FloatBuffer binormalBuffer = null;
		if (mesh.getBuffer(Type.Binormal) != null) {
			binormalBuffer = (FloatBuffer) mesh.getBuffer(Type.Binormal).getData();
		}

		ColorRGBA originColor = ColorRGBA.White;
		ColorRGBA tangentColor = ColorRGBA.Red;
		ColorRGBA binormalColor = ColorRGBA.Green;
		ColorRGBA normalColor = ColorRGBA.Blue;

		Mesh lineMesh = new Mesh();
		lineMesh.setMode(Mesh.Mode.Lines);

		Vector3f origin = new Vector3f();
		Vector3f point = new Vector3f();
		Vector3f tangent = new Vector3f();
		Vector3f normal = new Vector3f();

		IntBuffer lineIndex = BufferUtils.createIntBuffer(vertexBuffer.limit() / 3 * 6);
		FloatBuffer lineVertex = BufferUtils.createFloatBuffer(vertexBuffer.limit() * 4);
		FloatBuffer lineColor = BufferUtils.createFloatBuffer(vertexBuffer.limit() / 3 * 4 * 4);

		boolean hasParity = mesh.getBuffer(Type.Tangent).getNumComponents() == 4;
		float tangentW = 1;

		for (int i = 0; i < vertexBuffer.limit() / 3; i++) {
			populateFromBuffer(origin, vertexBuffer, i);
			populateFromBuffer(normal, normalBuffer, i);

			if (hasParity) {
				tangent.x = tangentBuffer.get(i * 4);
				tangent.y = tangentBuffer.get(i * 4 + 1);
				tangent.z = tangentBuffer.get(i * 4 + 2);
				tangentW = tangentBuffer.get(i * 4 + 3);
			} else {
				populateFromBuffer(tangent, tangentBuffer, i);
			}

			int index = i * 4;

			int id = i * 6;
			lineIndex.put(id, index);
			lineIndex.put(id + 1, index + 1);
			lineIndex.put(id + 2, index);
			lineIndex.put(id + 3, index + 2);
			lineIndex.put(id + 4, index);
			lineIndex.put(id + 5, index + 3);

			setInBuffer(origin, lineVertex, index);
			setInBuffer(originColor, lineColor, index);

			point.set(tangent);
			point.multLocal(scale);
			point.addLocal(origin);
			setInBuffer(point, lineVertex, index + 1);
			setInBuffer(tangentColor, lineColor, index + 1);

			// wvBinormal = cross(wvNormal, wvTangent) * -inTangent.w

			if (binormalBuffer == null) {
				normal.cross(tangent, point);
				point.multLocal(-tangentW);
				point.normalizeLocal();
			} else {
				populateFromBuffer(point, binormalBuffer, i);
			}

			point.multLocal(scale);
			point.addLocal(origin);
			setInBuffer(point, lineVertex, index + 2);
			setInBuffer(binormalColor, lineColor, index + 2);

			point.set(normal);
			point.multLocal(scale);
			point.addLocal(origin);
			setInBuffer(point, lineVertex, index + 3);
			setInBuffer(normalColor, lineColor, index + 3);
		}

		lineMesh.setBuffer(Type.Index, 1, lineIndex);
		lineMesh.setBuffer(Type.Position, 3, lineVertex);
		lineMesh.setBuffer(Type.Color, 4, lineColor);

		lineMesh.setStatic();
		// lineMesh.setInterleaved();
		return lineMesh;
	}
}
